6.3 方法集

类型有一个与之相关的方法集(method set),这决定了它是否实现某个接口。

  • 类型T方法集包含所有receiver T方法。
  • 类型*\T方法集包含所有receiver T+*\T方法。
  • 匿名嵌入S,T方法集包含所有receiver S方法。
  • 匿名嵌入*\S,T方法集包含所有receiver S+*S方法。
  • 匿名嵌入S或*\S,*\T方法集包含所有receiver S+*\S方法。

可利用反射(reflect)测试这些规则。

type S struct{} 
  
type T struct{ 
   S                  // 匿名嵌入字段 
} 
  
func(S)sVal()  {} 
func(*S)sPtr() {} 
func(T)tVal()  {} 
func(*T)tPtr() {} 
  
func methodSet(a interface{}) {           // 显示方法集里所有方法名字 
   t:=reflect.TypeOf(a) 
  
   for i,n:=0,t.NumMethod();i<n;i++ { 
       m:=t.Method(i) 
       fmt.Println(m.Name,m.Type) 
    } 
} 
  
func main() { 
   var t T
  
   methodSet(t)                 // 显示T方法集 
   println("----------") 
   methodSet(&t)                // 显示 *T方法集 
}
 

输出:

sVal func(main.T) 
tVal func(main.T) 
---------------------- 
sPtr func(*main.T) 
sVal func(*main.T) 
tPtr func(*main.T) 
tVal func(*main.T)

输出结果符合预期,但我们也注意到某些方法的receiver类型发生了改变。真实情况是,这些都是由编译器按方法集所需自动生成的额外包装方法。

$nm test|grep"main\." 
  
0000000000002040 t main.S.sVal
0000000000002050 t main.(*S).sPtr
00000000000023b0 t main.(*S).sVal
  
0000000000002570 t main.T.sVal
0000000000002060 t main.T.tVal
  
0000000000002490 t main.(*T).sPtr
0000000000002450 t main.(*T).sVal
0000000000002070 t main.(*T).tPtr
00000000000024d0 t main.(*T).tVal
  
  
$go tool objdump-s"main\."test|grep"TEXT.*autogenerated" 
  
TEXT main.(*S).sVal(SB) <autogenerated> 
  
TEXT main.T.sVal(SB)    <autogenerated> 
TEXT main.(*T).sVal(SB) <autogenerated> 
TEXT main.(*T).sPtr(SB) <autogenerated> 
TEXT main.(*T).tVal(SB) <autogenerated>

方法集仅影响接口实现和方法表达式转换,与通过实例或实例指针调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名)。

很显然,匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。

面向对象的三大特征“封装”、“继承”和“多态”,Go仅实现了部分特征,它更倾向于“组合优于继承”这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。

组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。

尽管接口也是多态的一种实现形式,但我认为应该和基于继承体系的多态分离开来。